<?php
/**
 * A rail_object has a user (owner) and a group. The mode specified the read and write rights for the user, the group
 * and all other. The rights managment is similar like the UNIX one.
 * The basic rail_object Object. Provides all basic methods like: get_id, copy, move, delete, get_inventory, get_path ..
 *
 * See the Documentation on http://www.open-rail.org for more informations.
 * It's possible to define own rail classes by extending the rail_object class.
 *
 * PHP versions 5
 *
 * Copyright (C)
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
 * of ERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software
 * Fundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA
 *
 * @author Bastian Schröder <bs@chemanet.de>
 * @version 2008-01-25
 * @package rail
 */
class rail_object {

	/**
	 * 9Bit binary word.
	 *
	 * @var string
	 */
	private $mode;

	/**
	 * Unique id for this object, generated by MySQL autoincrement colm.
	 *
	 * @var int
	 */
	private $rail_id;

	/**
	 * Object cache for this object. Used by some methods.
	 *
	 * @var array
	 */
	protected $cache = array();

	/**
	 * Attributes which are not writeable from outside. Outside means by calling set_attribute(s) method.
	 * The array contains only the name of protected attributes not the attribute itself.
	 *
	 * @var array
	 */
	protected $protected_attributes = array( OBJ_TIME_CREATE, OBJ_TIME_LASTWRITE );

	/**
	 * Relations which are not writeable from outside. Outside means by calling set_relation or remove_relation method.
	 * The array contains only the name of protected relation not the object associated with the relation.
	 *
	 * @var array
	 */
	protected $protected_relations = array();

	/**
	 * The rail_connector has an connection to the MySQL databases alocated with this object.
	 * You can't replace it.
	 *
	 * @var rail_connector
	 */
	protected $rail_connector;

	/**
	 * The type of this rail_object.
	 *
	 * @var string
	 */
	protected $type;


	/**
	 * Build this object from databases. Get mode and OBJ_NAME (hard) from databases.
	 *
	 * @exception rail_param_exception
	 * @param rail_connector $pAttribute
	 * @param int $pId
	 */
	public function __construct ( $pRailConnector, $pId ) {
		// check pRailConnector
		if ( !$pRailConnector instanceof rail_connector )
			throw new rail_param_exception( null, 'pRailConnector', $pRailConnector );

		$this->rail_connector = $pRailConnector;
		$this->rail_id = $pId;

		// get mode, type and OBJ_NAME direct
		$data = $this->rail_connector->send_request( "SELECT objects.`mode`,objects.`type`,objects_attributes.`value` FROM objects,objects_attributes WHERE objects.`id`=objects_attributes.`object` AND objects.`id`='$pId' AND objects_attributes.`attribute`='OBJ_NAME' LIMIT 1" )->fetch_assoc();
		$this->mode = $data[ 'mode' ];
		$this->type = strtoupper( $data[ 'type' ] );
		$this->cache[ 'attributes' ][OBJ_NAME] = $data[ 'value' ];
	}


	/**
	 * Flush the cache.
	 */
	public function cache_flush () {
		$this->cache = array();
	}


	/**
	 * Return a copy of this object.
	 *
	 * @exception rail_readpermission_exception
	 * @param rail_object $pEnvironment The new environment
	 * @param bool $pRecursive (optional)
	 * @return rail_object
	 */
	public function copy ( $pEnvironment, $pRecursive = false ) {
		// check read permission
		if( !$this->is_readable() )
		 	throw new rail_readpermission_exception( $this, $this->rail_connector->get_user() );


		$copy = rail_factory::create_object( $this->rail_connector, $pEnvironment, $this->get_name(), $this->type );

		// copy all attributes
		$copy->set_attributes( $this->get_all_attributes() );

		// copy readable inventory if requested
		if( $pRecursive ){
			foreach( $this->get_inventory() as $inventory ){
				if( $inventory->get_id() == $copy->get_id() || !$inventory->is_readable() )
					continue;

				$inventory->copy($copy);
			}
		}

		return $copy;
	}


	/**
	 * Count all objects in this objects inventory. Count recursive if requested.
	 *
	 * @exception rail_readpermission_exception
	 * @param string $pRailTypes (optional)
	 * @param bool $pRecursive (optional)
	 * @return int
	 */
	public function count_inventory ( $pRailTypes = '', $pRecursive = false ) {
		// check read permission
		 if( !$this->is_readable() )
		 	throw new rail_readpermission_exception( $this, $this->rail_connector->get_user() );

		$sql_types = '';
		if( strlen($pRailTypes) > 0 ){
			foreach( explode('|', $pRailTypes) as $type )
				$sql_types .= " OR `type`='$type'";

			$sql_types = ' AND(' . substr($sql_types, 4) . ')';
		}

		$size = 0;
		$data = $this->rail_connector->send_request( "SELECT `id` FROM objects WHERE `environment`='" . $this->get_id() . "'$sql_types" );

		while( $data_row = $data->fetch_assoc() ){
			$size ++ ;

			if( $pRecursive ){
				$obj = rail_factory::get_object( $this->rail_connector, $data_row['id'] );
				$size +=  $obj->count_inventory( $pRailTypes, $pRecursive );
			}
		}

		return $size;
	}


	/**
	 * Count all relations in with this object in involved.
	 *
	 * @exception rail_readpermission_exception
	 * @return int
	 */
	public function count_relations () {
		// check read permission
		 if( !$this->is_readable( $this->rail_connector->get_user() ) )
		 	throw new rail_readpermission_exception( $this, $this->rail_connector->get_user() );

		$size = 0;
		$data = $this->rail_connector->send_request( "SELECT type FROM objects_relations WHERE `object1`='" . $this->get_id() . "' OR `object2`='" . $this->get_id() . "'" );

		while( $data_row = $data->fetch_assoc() )
				$size ++ ;

		return $size;
	}


	/**
	 * Delete object and recursiv the whole inventory. If at least one object in inventory can't be deleted,
	 * the method throw an exception and dosn't delete this object.
	 *
	 * @exception rail_writepermission_exception
	 */
	public function delete () {
		// check write permission
		 if( !$this->is_writeable( $this->rail_connector->get_user() ) )
		 	throw new rail_writepermission_exception( $this, $this->rail_connector->get_user() );

		 // try to delete inventory recursiv.
		 // If one ore more objects from inventory can't deleted, don't delete this object
		 $catchedException = false;
		 foreach( $this->get_inventory() as $object ){
		 	try{
		 		$obj_return = $object->delete();
		 	}
		 	catch( Exception $e ){
		 		$catchedException = true;
		 	}
		 }

		 if( !$catchedException ){
		 	// if this object isn't a rail_link, try to delete all rail_link objects linked to this rail_object
		 	if( !$this instanceof rail_link )
		 		foreach( $this->get_relations( 'RAIL_LINK' ) as $object ) $object->delete();

			// delete object from sequences

			// delete all relations, attributes and the object itself
			$this->rail_connector->send_request( "
				DELETE FROM objects_relations WHERE `object1`='" . $this->get_id() . "' OR `object2`='" . $this->get_id() . "';
				DELETE FROM objects_attributes WHERE `object`='" . $this->get_id() . "';
				DELETE FROM objects WHERE `id`='" . $this->get_id() . "'
			" );
		}
		else throw new rail_writepermission_exception( $this, $this->rail_connector->get_user() );
	}


	/**
	 * Get all attributes saved by this object.
	 *
	 * @exception rail_readpermission_exception
	 * @return array
	 */
	public function get_all_attributes () {
		// check read permission
		if( !$this->is_readable() )
		 	throw new rail_readpermission_exception( $this, $this->rail_connector->get_user() );

		// flush attribute cache
		unset( $this->cache[ 'attributes' ] );
		$this->cache[ 'attributes' ] = array();

		// get all attributes from databases
		$data = $this->rail_connector->send_request( "SELECT `attribute`,`value` FROM objects_attributes WHERE `object`='" . $this->get_id() . "' ORDER BY `attribute`" );

		// add/update all attributes to attribute cache
		while( $data_row = $data->fetch_assoc() )
			$this->cache[ 'attributes' ][ $data_row[ 'attribute' ] ] = $data_row[ 'value' ];

		return $this->cache[ 'attributes' ];
	}


	/**
	 * Get one attribute. Return false if attribute dosn't exist.
	 *
	 * @param string $pAttribute
	 * @return mixed
	 */
	public function get_attribute ( $pAttribute ) {
		$this->get_attributes( array($pAttribute) );

		// check wheater attribute exist
		if( isset($this->cache[ 'attributes' ][$pAttribute ]) )
			return $this->cache[ 'attributes' ][$pAttribute ];

		return false;
	}


	/**
	 * Get attributes. Returned array of requested and found attributes.
	 *
	 * @exception rail_readpermission_exception
	 * @param array $pAttributes
	 * @return array
	 */
	public function get_attributes ( $pAttributes ) {
		// check read permission
		if( !$this->is_readable() )
		 	throw new rail_readpermission_exception( $this, $this->rail_connector->get_user() );

		$mysql_attributes = '';
		$requested_attributes = array();
		foreach( $pAttributes as $attribute ){
			$attribute = trim($attribute);

			// attribute found in attribute cache.
			if( isset( $this->cache[ 'attributes' ][$attribute ] ) )
				$requested_attributes[ $attribute ] = $this->cache[ 'attributes' ][ $attribute ];
			// otherwise add attribute to mysql select.
			else
				$mysql_attributes .= " OR `attribute`='$attribute'";
		}

		// select requested attributes from databases.
		if( strlen( $mysql_attributes ) > 0 ){
			$mysql_attributes = ' AND (' . substr( $mysql_attributes, 4 ) . ')';
			$data = $this->rail_connector->send_request( "SELECT `attribute`,`value` FROM objects_attributes WHERE `object`='" . $this->get_id() . "'" . $mysql_attributes );

			// add selected attributes to attribute cache
			while( $data_row = $data->fetch_assoc() ){
				$this->cache[ 'attributes' ][ $data_row[ 'attribute' ] ] = $data_row[ 'value' ];
				$requested_attributes[ $data_row[ 'attribute' ] ] = $data_row[ 'value' ];
			}
		}

		return $requested_attributes;
	}


	/**
	 * Get group from this object.
	 *
	 * @return rail_group
	 */
	public function get_group () {
		if( !isset( $this->cache[ 'group' ] ) ){
			$data = $this->rail_connector->send_request( "SELECT `group` FROM objects WHERE `id`='" . $this->get_id() . "' LIMIT 1" )->fetch_assoc();
			$this->cache[ 'group' ] = rail_factory::get_object( $this->rail_connector, $data[ 'group' ] );
		}

		return $this->cache[ 'group' ];
	}


	/**
	 * Get current environment (parent node).
	 * Environment is that object who has this object in his inventory.
	 *
	 * @return rail_object
	 */
	public function get_environment () {
		// get environment from databases and store to cache
		if( !isset( $this->cache[ 'environment' ] ) ){
			$data = $this->rail_connector->send_request( "SELECT `environment` FROM objects WHERE `id`='" . $this->get_id() . "' LIMIT 1" )->fetch_assoc();
			$this->cache[ 'environment' ] = rail_factory::get_object( $this->rail_connector, $data[ 'environment' ] );
		}

		// return cached environment
		return $this->cache[ 'environment' ];
	}


	/**
	 * Get rail_id.
	 *
	 * @return int
	 */
	public function get_id () {
		return $this->rail_id;
	}


	/**
	 * Get alle objects who has this object as environment. Returned array of rail_object.
	 *
	 * @exception rail_readpermission_exception
	 * @param string $pRailType RAIL_TYPE|RAIL_TYPE (optional)
	 * @param string $pOrderByAttribte (optional)
	 * @param bool $pRecursive (optional)
	 * @return array
	 */
	public function get_inventory ( $pRailTypes = '', $pOrderByAttribte = OBJ_NAME , $pRecursive = false ) {
		// check read permission
		 if( !$this->is_readable() )
		 	throw new rail_readpermission_exception( $this, $this->rail_connector->get_user() );

		$objects = array();

		$sql_types = '';
		if( strlen($pRailTypes) > 0 ){
			foreach( explode('|', $pRailTypes) as $type )
				$sql_types .= " OR `type`='$type'";

			$sql_types = ' AND(' . substr($sql_types, 4) . ')';
		}

		$data = $this->rail_connector->send_request( "SELECT `id` FROM objects WHERE `environment`='" . $this->get_id() . "'$sql_types ORDER BY `id`" );

		// get all inventory objects
		while( $data_row = $data->fetch_assoc() ){
			$obj = rail_factory::get_object( $this->rail_connector,  $data_row[ 'id' ] );
			$objects[] = $obj;

			if( $pRecursive )
				$objects = array_merge( $objects, $obj->get_inventory( $pRailTypes, $pOrderByAttribte, $pRecursive ) );
		}

		// sort $objects
		$objects_sorted = array();
		foreach( $objects as $object ){
			if( !$object->is_readable() ){
				$objects_sorted[] = $object;
				continue;
			}

			// modified bubble sort
			for( $i = 0; $i < count( $objects_sorted ); $i ++ ) {
				if( !$objects_sorted[ $i ]->is_readable( $this->rail_connector->get_user() ) )
					continue;

				if(
					$objects_sorted[ $i ]->get_attribute( $pOrderByAttribte ) > $object->get_attribute( $pOrderByAttribte ) ||
					(
						$objects_sorted[ $i ]->get_attribute( $pOrderByAttribte ) == $object->get_attribute( $pOrderByAttribte ) &&
						$objects_sorted[ $i ]->get_id() > $object->get_id()
					)
				){
					$tmp = 	$objects_sorted[ $i ];
					$objects_sorted[ $i ] = $object;
					$object = $tmp;
				}
			}
			$objects_sorted[] = $object;
		}

		return $objects_sorted;
	}


	/**
	 * Get mode as int.
	 *
	 * @return int
	 */
	public function get_mode () {
		return 	bindec( substr( $this->mode, 0, 3 ) ) .
				bindec( substr( $this->mode, 3, 3 ) ) .
				bindec( substr( $this->mode, 6, 3 ) );
	}


	/**
	 * Get value of attribute OBJ_NAME.
	 *
	 * @return string
	 */
	public function get_name () {
		if( !isset( $this->cache[ 'attributes' ][ OBJ_NAME ] ) )
			$this->get_attributes( array( OBJ_NAME ) );
		return $this->cache[ 'attributes' ][ OBJ_NAME ];
	}


	/**
	 * Get rail_path for this object.
	 *
	 * @return string
	 */
	public function get_path () {
		// if path isn't cached
		if( !isset( $this->cache[ 'path' ] ) ){
			$data = $this->rail_connector->send_request( "SELECT `id` FROM objects WHERE `environment`='" . $this->get_environment()->get_id() . "' ORDER BY `id`" );
			$position = 1;
			// get all inventory objects
			while( $data_row = $data->fetch_assoc() ){
				$id = $data_row[ 'id' ];

				if( $id == $this->get_id() ){
					// cached the path
					$this->cache[ 'path' ] = $this->get_environment()->get_path() . urlencode( $this->get_name() ) . ( $position > 1 ? '[' . $position . ']' : '' ) . '/';
					break;
				}

				$object = rail_factory::get_object( $this->rail_connector, $id );

				// increment position
				if( $object->get_name() == $this->get_name() )
					$position ++ ;
			}
		}

		// return cached path
		return $this->cache[ 'path' ];
	}


	/**
	 * Get all relation types.
	 *
	 * @exception rail_readpermission_exception
	 * @return array
	 */
    public function get_relation_types () {
		// check read permission
		if( !$this->is_readable() )
		 	throw new rail_readpermission_exception( $this, $this->rail_connector->get_user() );

		$relations = array();

		$data = $this->rail_connector->send_request( "SELECT DISTINCT type FROM objects_relations WHERE `object1`='" . $this->get_id() . "' OR `object2`='" . $this->get_id() . "' ORDER BY type" );

		while( $data_row = $data->fetch_assoc() )
			$relations[] = $data_row[ 'type' ];

		return $relations;
	}


	/**
	 * Get all relations for a specifid type. Returned an array of rail_objects.
	 *
	 * @exception rail_readpermission_exception
	 * @param string $pType
	 * @return array
	 */
	public function get_relations ( $pType ) {
		// check read permission
		if( !$this->is_readable() )
		 	throw new rail_readpermission_exception( $this, $this->rail_connector->get_user() );

		$type = trim( $pType );

		$objects = array();

		$data = $this->rail_connector->send_request( "SELECT `object1`,`object2` FROM objects_relations WHERE `type`='$type' AND (`object1`='" . $this->get_id() . "' OR `object2`='" . $this->get_id() . "') ORDER BY object1,object2" );

		while( $data_row = $data->fetch_assoc() ){
			$object_id = $data_row[ 'object1' ] == $this->get_id() ? $data_row[ 'object2' ] : $data_row[ 'object1' ];
			$objects[] = rail_factory::get_object( $this->rail_connector, $object_id );
		}

		return $objects;
	}


	/**
	 * Get the type for this rail_object.
	 *
	 * @return string
	 */
	public function get_type () {
		return $this->type;
	}


	/**
	 * Get user from this object.
	 *
	 * @return rail_user
	 */
	public function get_user () {
		if( !isset( $this->cache[ 'user' ] ) ){
			$data = $this->rail_connector->send_request( "SELECT `user` FROM objects WHERE `id`='" . $this->get_id() . "' LIMIT 1" )->fetch_assoc();
			$this->cache[ 'user' ] = rail_factory::get_object( $this->rail_connector, $data[ 'user' ] );
		}

		return $this->cache[ 'user' ];
	}


	/**
	 * Move object to new environment. Use must have write permission on this objects as well on pRailObject.
	 *
	 * @exception rail_writepermission_exception
	 * @param rail_object $pRailObject
	 */
	public function move ( $pRailObject ) {
		// check write permission
		if( !$this->is_writeable() )
			throw new rail_writepermission_exception( $this, $this->rail_connector->get_user() );
		// check new environment write permission
		if( !$pRailObject->is_writeable() )
		 	throw new rail_writepermission_exception( $this, $this->rail_connector->get_user(), 'can\'t write to new environment.' );
		 // cant't move objet into it self subtree
		 if( substr( $pRailObject->get_path(), 0, strlen( $this->get_path() ) ) == $this->get_path() )
		 	throw new rail_writepermission_exception( $this, $this->rail_connector->get_user(), 'can\'t move into own inventory.' );

		$this->rail_connector->send_request( "UPDATE objects SET `environment`='" . $pRailObject->get_id() . "' WHERE `id`='" . $this->get_id() . "'" );
		$this->cache[ 'environment' ] = $pRailObject;

		// remove cached path
		if( isset($this->cache[ 'path' ]) )
			unset( $this->cache[ 'path' ] );

		// touch this rail_object
		$this->touch();
	}


	/**
	 * Chekced wheater an attribute is prottected.
	 *
	 * @param string $pAttribute
	 * @return bool
	 */
	public function is_attribute_protected ( $pAttribute ) {
		return in_array( trim($pAttribute), $this->protected_attributes );
	}


	/**
	 * Checked wheater this object is readable for a user.
	 *
	 * @param rail_user $pRailUser (optional)
	 * @return bool
	 */
	public function is_readable ( $pRailUser = null ) {
		$rail_user = $pRailUser == null ? $this->rail_connector->get_user() : $pRailUser;

		// root user has grand access
		if( $rail_user->get_name() == 'root' )
			return true;

		$user =  strrev( substr( $this->mode, 0, 3 ) );
		$group = strrev( substr( $this->mode, 3, 3 ) );
		$other = strrev( substr( $this->mode, 6, 3 ) );

		// access if all users has read privileges
		if( ($other & 1) )
			return true;

		// access if user is user of this object and the user has read privileges
		if( $rail_user->get_id() == $this->get_user()->get_id() && ($user & 1) )
			return true;

		// access if user is in this objects group and the group has read privileges
		$data = $this->rail_connector->send_request( "SELECT `object1`,`object2` FROM objects_relations WHERE `type`='RAIL_MEMBERSHIP' AND `object1`='" . $this->get_group()->get_id() . "' AND `object2`='" . $rail_user->get_id() . "' LIMIT 1" )->fetch_assoc();
		if( ( $rail_user->get_group()->get_id() == $this->get_group()->get_id() || count( $data ) > 0 ) &&  ($group & 1) )
		 	return true;

		return false;
	}


	/**
	 * Checked wheater a relation is set for an rail_object.
	 *
	 * @exception rail_readpermission_exception
	 * @param rail_object $pRailObject
	 * @param string $pType
	 */
	public function is_relation ( $pRailObject, $pType ) {
		// check read permission
		if( !$this->is_readable() )
		 	throw new rail_readpermission_exception( $this, $this->rail_connector->get_user() );

		$type = trim( $pType );

		$data = $this->rail_connector->send_request( "SELECT `object1`,`object2` FROM objects_relations WHERE `type`='$type' AND (`object1`='" . $this->get_id() . "' OR `object2`='" . $this->get_id() . "') AND (`object1`='" . $pRailObject->get_id() . "' OR `object2`='" . $pRailObject->get_id() . "') LIMIT 1" )->fetch_assoc();

		return count( $data ) > 0;
	}


	/**
	 * Checked wheater this object is writeable for a user.
	 *
	 * @param rail_user $pRailUser (optional)
	 * @return bool
	 */
	public function is_writeable ( $pRailUser = null ) {
		$rail_user = $pRailUser == null ? $this->rail_connector->get_user() : $pRailUser;

		// root user has grand access
		if( $rail_user->get_name() == 'root' )
			return true;

		$user =  strrev( substr( $this->mode, 0, 3 ) );
		$group = strrev( substr( $this->mode, 3, 3 ) );
		$other = strrev( substr( $this->mode, 6, 3 ) );

		// access if all users has write privileges
		if( ($other & 2) )
			return true;

		// access if user is user of this object and the user has write privileges
		if( $rail_user->get_id() == $this->get_user()->get_id() && ($user & 2) )
			return true;

		// access if user is in this objects group and the group has write privileges
		$data = $this->rail_connector->send_request( "SELECT `object1`,`object2` FROM objects_relations WHERE `type`='RAIL_MEMBERSHIP' AND `object1`='" . $this->get_group()->get_id() . "' AND `object2`='" . $rail_user->get_id() . "' LIMIT 1" )->fetch_assoc();
		if( ( $rail_user->get_group()->get_id() == $this->get_group()->get_id() || count( $data ) > 0 ) &&  ($group & 2) )
		 	return true;

		return false;
	}


	/**
	 * Remove attribute.
	 *
	 * @exception rail_writepermission_exception
	 * @param string $pAttribute
	 */
	public function remove_attribute ( $pAttribute ) {
		$attribute = trim($pAttribute);

		// check write permission
		if( !$this->is_writeable() )
			throw new rail_writepermission_exception( $this, $this->rail_connector->get_user() );

		// protected attributes can't be remove from not intern the PHPrail.
		// It's already forbidden to remove OBJ_NAME attribute.
		$stack = debug_backtrace();
		if(
			(
				in_array( $attribute, $this->protected_attributes )
				&& (
					count($stack) <= 1															// if called extern
					|| ( count($stack) == 2 && $stack[ 1 ][ 'function' ] == 'set_attribute' )	// via set_attribute but extern
					|| substr($stack[ count($stack) > 2 ? 2 : 1 ]['class'], 0, 5) != 'rail_'	// called not intern
				)
			)
			|| $attribute == OBJ_NAME															// OBJ_NAME can't deleted
		)
			throw new rail_writepermission_exception( $this, $this->rail_connector->get_user(), "can't remove protected attribute '$attribute'." );

		// remove attribute and attribute from cache
		$this->rail_connector->send_request( "DELETE FROM objects_attributes WHERE `object`='" . $this->get_id() . "' AND `attribute`='$attribute'" );
		unset(  $this->cache[ 'attributes' ][ $attribute ] );

		// touch this rail_object
		$this->touch();
	}


	/**
	 * Remove one relation.
	 *
	 * @exception rail_writepermission_exception
	 * @param rail_object
	 * @param string type
	 */
	public function remove_relation ( $pRailObject, $pType ) {
		// check write permission
		if( !$this->is_writeable() )
		 	throw new rail_writepermission_exception( $this, $this->rail_connector->get_user() );

		$type = trim( $pType );

		// protected relations cant' be remove if called not intern from PHPrail
		$stack = debug_backtrace();
		if(
			in_array( $type, $this->protected_relations )
			&& (
				count($stack) <= 1															// if called extern
				|| substr($stack[ count($stack) > 2 ? 2 : 1 ]['class'], 0, 5) != 'rail_'	// called not intern
			)
		)
			throw new rail_writepermission_exception( $this, $this->rail_connector->get_user(), 'relation: $type is protected.' );

		$this->rail_connector->send_request( "DELETE FROM objects_relations WHERE `type`='$type' AND (`object1`='" . $this->get_id() . "' OR `object2`='" . $this->get_id() . "') AND (`object1`='" . $pRailObject->get_id() . "' OR `object2`='" . $pRailObject->get_id() . "') LIMIT 1" );

		// touch this rail_object
		$this->touch();
	}


	/**
	 * Set attribute. A Wrapper for set_attributes.
	 *
	 * @param string $pAttributes
	 * @param string $pValue
	 */
	public function set_attribute ( $pAttribute, $pValue ) {
		$this->set_attributes( array( $pAttribute => $pValue ) );
	}


	/**
	 * Set attributes. Update or add attributes for this rail_object.
	 * Some attributes may be protected that means this attributes are not writeable directly.
	 *
	 * @exception rail_writepermission_exception
	 * @param array $pAttributes
	 */
	public function set_attributes ( $pAttributes ) {
		 // check write permission
		 if( !$this->is_writeable() )
		 	throw new rail_writepermission_exception( $this, $this->rail_connector->get_user() );

		$forbidden_attributes = array();

		foreach( $pAttributes as $attribute => $value ){
			$attribute =  trim( $attribute );
			$value = rtrim( $value );

			// protected attributes will only be updated if called from intern from PHPrail
			$stack = debug_backtrace();
			if(
				(
					in_array( $attribute, $this->protected_attributes )
					&& (
						count($stack) <= 1															// if called extern
						|| ( count($stack) == 2 && $stack[ 1 ][ 'function' ] == 'set_attribute' )	// via set_attribute but extern
						|| substr($stack[ count($stack) > 2 ? 2 : 1 ]['class'], 0, 5) != 'rail_'	// called not intern
					)
				)
				|| ( $attribute == OBJ_NAME && strlen( $value ) == 0 )								// OBJ_NAME can't set to empty string
			){
				$forbidden_attributes[] = $attribute;
				continue;
			}

			// get old value
			$this->get_attribute( $attribute );

			// return if value dosn't change
			if( isset( $this->cache[ 'attributes' ][ $attribute ] ) && $this->cache[ 'attributes' ][ $attribute ] == $value )
				continue;

			// update attribute in database if exist.
			if( isset( $this->cache[ 'attributes' ][ $attribute ] ) )
				$this->rail_connector->send_request( "UPDATE objects_attributes SET `value`='" . addslashes( $value ) . "' WHERE `object`='" . $this->get_id() . "' AND `attribute`='$attribute'" );

			// otherwise add new attribute to database.
			else
				$this->rail_connector->send_request( "INSERT INTO objects_attributes (`object`,`attribute`,`value`) VALUES ('" . $this->get_id() . "','$attribute','" . addslashes( $value ) . "')" );

			// update attribute cache
			$this->cache[ 'attributes' ][ $attribute ] = $value;

			// if updated OBJ_NAME attribute remove path from cache
			if( $attribute == 'OBJ_NAME' )
				unset( $this->cache[ 'path' ] );
		}

		// touch this rail_object if updated at least one attribute
		if( count( $pAttributes ) > count( $forbidden_attributes ) )
			$this->touch();

		// throw exception if trying to update protected attributes
		if( count( $forbidden_attributes ) > 0 )
			throw new rail_writepermission_exception( $this, $this->rail_connector->get_user(), 'attribute(s): ' . implode( ', ', $forbidden_attributes ) . ' are protected.' );
	}


	/**
	 * Set group for this rail_object. Optional called recursive for whole inventory.
	 *
	 * @exception rail_param_exception|rail_writepermission_exception
	 * @param rail_group $pRailGroup
	 * @param bool $pRecursive (optional)
	 */
	public function set_group ( $pRailGroup, $pRecursive = false ) {
		// check write permission
		if(
			$this->rail_connector->get_user()->get_name() != 'root' // root has grand access
			&& $this->rail_connector->get_user()->get_id() != $this->get_user()->get_id() // user has access
		)
			throw new rail_writepermission_exception( $this, $this->rail_connector->get_user() );
		// check wheather pRailUser is a valid rail_user
		if( !$pRailGroup instanceof rail_group )
			throw new rail_param_exception( $this, 'pRailGroup', $pRailGroup );
		// check wheater pRailUser is a member of new group
		if( !$pRailGroup->is_member( $this->rail_connector->get_user() ) )
			throw new rail_param_exception( $this, 'pRailGroup', $pRailGroup, 'current loged in user is not a member of this group.' );

		// update group and remove cached group
		$this->rail_connector->send_request( "UPDATE objects SET `group`='" . $pRailGroup->get_id() . "' WHERE `id`='" . $this->get_id() ."' LIMIT 1" );
		unset( $this->cache[ 'group' ] );

		// if requested call recursive for whole inventory
		if( $pRecursive )
			foreach( $this->get_inventory() as $object ) $object->set_group( $pRailGroup, $pRecursive );

		// touch this rail_object
		$this->touch();
	}


	/**
	 * Set mode for this object. If requested called recursive for whole inventory.
	 *
	 * @exception rail_writepermission_exception
	 * @param int $pMode
	 * @param bool $pRecursive (optional)
	 */
	public function set_mode ( $pMode, $pRecursive = false ) {
		// check write permission
		if(
			$this->rail_connector->get_user()->get_name() != 'root' // root has grand access
			&& $this->rail_connector->get_user()->get_id() != $this->get_user()->get_id() // user has access
		)
			throw new rail_writepermission_exception( $this, $this->rail_connector->get_user() );

		$mode = substr( decbin( substr($pMode, 0, 1) ) . '000', 0, 3 ) .
				substr( decbin( substr($pMode, 1, 1) ) . '000', 0, 3 ) .
				substr( decbin( substr($pMode, 2, 1) ) . '000', 0, 3 );

		$this->rail_connector->send_request( "UPDATE objects SET `mode`='$mode' WHERE `id`='" . $this->get_id() . "' LIMIT 1" );
		$this->mode = $mode;

		// if requested call recursive for whole inventory
		if( $pRecursive )
			foreach( $this->get_inventory() as $object ) $object->set_mode( $pMode, $pRecursive );

		// touch this rail_object
		$this->touch();
	}


	/**
	 * Set a new relation to this object. If trying to add a already existing relation do nothing.
	 *
	 * @exception rail_writepermission_exception
	 * @param rail_object $pRailObject
	 * @param string $pType
	 */
	public function set_relation ( $pRailObject, $pType ) {
		// check write permission
		if( !$this->is_writeable() )
		 	throw new rail_writepermission_exception( $this, $this->rail_connector->get_user() );

		$type = trim( $pType );

		// protected relations will be only updated if called intern from PHPrail
		$stack = debug_backtrace();
		if(
			in_array( $type, $this->protected_relations )
			&& (
				count($stack) <= 1															// if called extern
				|| substr($stack[ count($stack) > 2 ? 2 : 1 ]['class'], 0, 5) != 'rail_'	// called not intern
			)
		)
			throw new rail_writepermission_exception( $this, $this->rail_connector->get_user(), 'relation: $type is protected.' );

		// try to finger out existing relation
		$data = $this->rail_connector->send_request( "SELECT `object1`,`object2` FROM objects_relations WHERE `type`='$type' AND (`object1`='" . $this->get_id() . "' OR `object2`='" . $this->get_id() . "') AND (`object1`='" . $pRailObject->get_id() . "' OR `object2`='" . $pRailObject->get_id() . "') LIMIT 1" )->fetch_assoc();

		// only add if relation dosn't exist.
		if( count( $data ) <= 0 )
			$this->rail_connector->send_request( "INSERT INTO objects_relations (`object1`,`object2`,`type`) VALUES ('" . $this->get_id() . "','" . $pRailObject->get_id() . "','$type')" );

		// touch this rail_object
		$this->touch();
	}


	/**
	 * Set user for this rail_object. Optional also set group for rail_object and called recursive for whole inventory.
	 * Only the root user can call this method.
	 *
	 * @exception rail_param_exception|rail_writepermission_exception
	 * @param rail_user $pRailUser
	 * @param rail_group $pRailGroup (optional)
	 * @param bool $pRecursive (optional)
	 */
	public function set_user ( $pRailUser, $pRailGroup = null, $pRecursive = false ) {
		// check write permission
		if( $this->rail_connector->get_user()->get_name() != 'root' )
			throw new rail_writepermission_exception( $this, $this->rail_connector->get_user() );
		// check wheather pRailUser is a valid rail_user
		if( !$pRailUser instanceof rail_user )
			throw new rail_param_exception( $this, 'pRailUser', $pRailUser );

		// update user
		$this->rail_connector->send_request( "UPDATE objects SET `user`='" . $pRailUser->get_id() . "' WHERE `id`='" . $this->get_id() ."' LIMIT 1" );
		unset( $this->cache[ 'user' ] );

		// if pRailGroup is a valid rail_group instance, update group.
		if( $pRailGroup instanceof rail_group ) {
			$this->rail_connector->send_request( "UPDATE objects SET `group`='" . $pRailGroup->get_id() . "' WHERE `id`='" . $this->get_id() ."' LIMIT 1" );
			unset( $this->cache[ 'group' ] );
		}

		// if requested call recursive for inventory
		if( $pRecursive )
			foreach( $this->get_inventory() as $object ) $object->set_user( $pRailUser, $pRailGroup, $pRecursive );

		// touch this rail_object
		$this->touch();
	}


	/**
	 * Touch this rail_object. Update OBJ_TIME_LASTWRITE.
	 *
	 * @exception rail_writepermission_exception
	 */
	public function touch () {
		// check write permission
		if( !$this->is_writeable() )
		 	throw new rail_writepermission_exception( $this, $this->rail_connector->get_user() );

		// update protected attribute OBJ_TIME_LASTWRITE
		$this->rail_connector->send_request( "UPDATE objects_attributes SET `value`='" . time() . "' WHERE `object`='" . $this->get_id() . "' AND `attribute`='OBJ_TIME_LASTWRITE'" );
	}

} // End of rail_object
?>